#!/usr/bin/python

from optparse import OptionParser
import glob
import os
import re
import sys
from xml.dom.minidom import Document

# moved paramiko to collect_data() to avoid importing it unless user is collecting
# data from an on-array run.  Need to make sure that it's legal to add paramiko to kit.
#import paramiko

class DataFoundError(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)    


class SSHError(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class CoverageTree:

    def __init__(self, full_path, lines_count=0, lines_covered=0,
                 functions_count=0, functions_coverd=0,
                 calls_count=0, calls_coverd=0,
                 branches_count=0, branches_coverd=0):
        self._tree = []
        self._name = full_path
        self._lines_count = lines_count
        self._lines_coverd = lines_covered
        self._functions_count = functions_count
        self._functions_coverd = functions_coverd
        self._calls_count = calls_count
        self._calls_coverd = calls_coverd
        self._branches_count = branches_count
        self._branches_coverd = branches_coverd
        self._max_len = 0


    def add_leaf(self, leaf):

        if not leaf._name.startswith(self._name + '/'):
            return False

        self._lines_count += leaf._lines_count
        self._lines_coverd += leaf._lines_coverd
        self._functions_count += leaf._functions_count
        self._functions_coverd += leaf._functions_coverd
        self._calls_count += leaf._calls_count
        self._calls_coverd += leaf._calls_coverd
        self._branches_count += leaf._branches_count
        self._branches_coverd += leaf._branches_coverd

        leaf._name = leaf._name.replace(self._name + '/', '')
        for branch in self._tree:
            if (branch.add_leaf(leaf) == True):
                return True
        index = leaf._name.find('/')
        if index != -1:
            temp = CoverageTree(leaf._name[:index])
            temp.add_leaf(leaf)
            if (self._max_len < len(temp._name)):
                self._max_len = len(temp._name)
            self._tree.append(temp)
        else:
            if (self._max_len < len(leaf._name)):
                self._max_len = len(leaf._name)
            self._tree.append(leaf)
        return True


    def create_xml_node(self, parent, just=0, depth=-1, metrics=['l']):

        sources = parent.createElement(self._name.replace('/', '_').ljust(just))
        if 'l' in metrics:
            sources.setAttribute("lines", "% 6d/% 6d=%5.1f%%" % (self._lines_coverd, self._lines_count, (self._lines_coverd / float(self._lines_count) * 100) if (self._lines_count != 0) else 0))
        if 'b' in metrics:
            sources.setAttribute("branches", "% 6d/% 6d=%5.1f%%" % (self._branches_coverd, self._branches_count, (self._branches_coverd / float(self._branches_count) * 100) if (self._branches_count != 0) else 0))
        if 'f' in metrics:
            sources.setAttribute("functions", "% 6d/% 6d=%5.1f%%" % (self._functions_coverd, self._functions_count, (self._functions_coverd / float(self._functions_count) * 100)if (self._functions_count != 0) else 0))
        if 'c' in metrics:
            sources.setAttribute("calls", "% 6d/% 6d=%5.1f%%" % (self._calls_coverd, self._calls_count, (self._calls_coverd / float(self._calls_count) * 100)if (self._calls_count != 0) else 0))

        if ((len(self._tree) > 0) and (depth != 0)):
            for branch in self._tree:
                sources.appendChild(branch.create_xml_node(parent, self._max_len, depth - 1, metrics))
        return sources

    
    def create_xml_report(self, depth=-1, metrics=['l']):
        
        doc = Document()
        root = doc.createElement("coverage")
        root.appendChild(self.create_xml_node(doc, depth=depth, metrics=metrics))
        doc.appendChild(root)
        return doc


    def create_plain_node(self, indent='', just=0, depth=-1, metrics=['l']):
        output = ''
        output += indent + self._name.ljust(just)
        if 'l' in metrics:
            output += "% 6d/% 6d=%5.1f%% " % (self._lines_coverd, self._lines_count, (self._lines_coverd / float(self._lines_count) * 100) if (self._lines_count != 0) else 0)
        if 'b' in metrics:
            output += "% 6d/% 6d=%5.1f%% " % (self._branches_coverd, self._branches_count, (self._branches_coverd / float(self._branches_count) * 100)if (self._branches_count != 0) else 0)
        if 'f' in metrics:
            output += "% 6d/% 6d=%5.1f%% " % (self._functions_coverd, self._functions_count, (self._functions_coverd / float(self._functions_count) * 100)if (self._functions_count != 0) else 0)
        if 'c' in metrics:
            output += "% 6d/% 6d=%5.1f%% " % (self._calls_coverd, self._calls_count, (self._calls_coverd / float(self._calls_count) * 100)if (self._calls_count != 0) else 0)
        output += '\n'

        if ((len(self._tree) > 0) and (depth != 0)):
            output += indent + '    ' + "".ljust(self._max_len, '-')
            if 'l' in metrics:output += "Lines covered ".rjust(21, '-')
            if 'b' in metrics:output += "Branches covered ".rjust(21, '-')
            if 'f' in metrics:output += "Functions covered ".rjust(21, '-')
            if 'c' in metrics:output += "Calls covered ".rjust(21, '-')
            output += '\n'
            for branch in self._tree:
                output += branch.create_plain_node(indent + '    ', self._max_len, depth - 1, metrics)
        return output


    def create_plain_report(self, depth=-1, metrics=['l']):
        output = "".ljust(len(self._name), '-')
        if 'l' in metrics:
            output += "Lines covered ".rjust(21, '-')
        if 'b' in metrics:
            output += "Branches covered ".rjust(21, '-')
        if 'f' in metrics:
            output += "Functions covered ".rjust(21, '-')
        if 'c' in metrics:
            output += "Calls covered ".rjust(21, '-')
        output += '\n'
        output += self.create_plain_node(depth=depth, metrics=metrics)
        return output


def main():

    parser = OptionParser(usage="This utility allows collect covarage data.")
    parser.add_option("-p", "--password", dest="password",
                      help="password to be used to login into storage system. Defaults to c4proto!.", default="c4proto!")
    parser.add_option("-k", "--key", dest="private_key",
                      help="file name of a private key file to be used instead of specifying plain text password.",metavar="KEY", default=None)
    parser.add_option("-s", "--system", dest="ips",
                      help="comma separated list of IP addresses for ssh interfaces of storage system storage processors. If configured data is copied fom system. Else using existing data",metavar="IPS", default=[])
    parser.add_option("-u", "--user", dest="username",
                      help="username to be used to login into storage system. Defaults to root.",metavar="USER", default="root")
    parser.add_option("-m", "--metrics", dest="metrics",
                      help='''comma separated list of metrics to be displayed. You can provide following strings: "lines", "branches", "calls", "functions", "l", "b", "f", "c"''',metavar="METRICS", default=[])
    parser.add_option("-x", "--xml", dest="xml",
                      help="if specified report created in xml format", action="store_true", default=False)
    parser.add_option("-r", "--reports", dest="save_reports",
                      help="generate more detailed GCOV coverage reports for individual source code files", action="store_true", default=False)
    parser.add_option("-c", "--clear", dest="clear",
                      help="if specified script clear all gcov data", action="store_true", default=False)
    parser.add_option("-d", "--depth", dest="depth", type = "int",
                      help="subdirectories level that will be included in report",metavar="DEPTH", default=-1)

    (options, args) = parser.parse_args()

    ips = []
    if options.ips != []:
        ips = options.ips.split(',')
        for ip in ips:
            if not re.match('''([0-9]*\.){3}[0-9]*''', ip):
                print "Incorrect IP address " + ip
                sys.exit(2)
    options.ips = ips
    
    metrics = []
    if options.metrics != []:
        for column in options.metrics.split(','):
            if ((column in ["lines", "branches", "calls", "functions", "l", "b", "f", "c"]) and (not column[0] in metrics)):
                metrics.append(column[0])
    if metrics == []:
        metrics = ['l']
    options.metrics = metrics

    try:
        original_folder = os.getcwd()
        search = True

        if (options.ips != []):
            current_folder = '/' + os.getcwd().split('/')[1]
            collect_data(options.ips, options.username, options.password, options.private_key, current_folder)
            search = False

        files = find_data(original_folder, search)

        cover = process_data(files, options.save_reports)


        report = ''
        if options.xml:
            report = cover.create_xml_report(depth=options.depth, metrics=options.metrics).toprettyxml(indent="  ")
        else:
            report = cover.create_plain_report(depth=options.depth, metrics=options.metrics)

        print report

        if options.clear:
            clear_folder(original_folder)

    except DataFoundError as e:
        print str(e)
        sys.exit(2)

    except SSHError as e:
        print str(e)
        sys.exit(2)


def clear_folder(folder):
    for folder in determine_source_folders(folder):
        cmd = " find " + folder + " -name '*.gc*' | xargs rm"
        os.popen(cmd)

    
def process_data(files, save_files=False):
    sys.stderr.write("Processing %s files..." % len(files))
    cover = CoverageTree(os.getcwd())
    gcov_files = []
    for file in files:
        os.chdir(os.path.dirname(file))
        cmd = " gcov " + file + " --branch-counts --branch-probabilities  2>/dev/null"
        os.popen(cmd)
        gcov = open(file + '.gcov', "r")
        lines_count = 0
        lines_covered = 0
        functions_count = 0
        functions_coverd = 0
        calls_count = 0
        calls_coverd = 0
        branches_count = 0
        branches_coverd = 0
        for line in gcov:
            segments = line.split(":", 2)
            tmp = segments[0].strip()

            if tmp[0] == '#':
                lines_count += 1
            elif tmp[0] in "0123456789":
                lines_count += 1
                lines_covered += 1
            elif tmp[0] == '-':
                pass
            elif tmp.startswith('branch'):
                branches_count += 1
                fields = line.split()
                if (fields[2] != 'never'):
                    if (int(fields[3]) > 0):
                        branches_coverd += 1
            elif tmp.startswith('call'):
                calls_count += 1
                fields = line.split()
                if (fields[2] != 'never'):
                    if (int(fields[3]) > 0):
                        calls_coverd += 1
            elif tmp.startswith('function'):
                functions_count += 1
                index = tmp.find('called')
                if ((index != -1) and (tmp[index + 7] != '0')):
                    functions_coverd += 1
            elif tmp[0] == 'f':
                pass
        cover.add_leaf(CoverageTree(file, lines_count, lines_covered,
                       functions_count, functions_coverd,
                       calls_count, calls_coverd,
                       branches_count, branches_coverd))
        gcov.close()
        if save_files: gcov_files.append(file + '.gcov')
        for filename in glob.glob('*.gcov'):
            if ((not save_files) or (not (os.path.abspath(filename) in gcov_files))):
                os.remove(filename)
    return cover


def find_data(current_folder, search=False):
    output = []
    if search:
        sys.stderr.write("Searching coverage files...")
        for folder in determine_source_folders(current_folder):
            cmd = "find " + folder + "  -name '*.gcda' "
            result = os.popen(cmd).read()
            output.extend(filter(None, result.replace('gcda', 'cpp').split('\n')))
            if len(result) == 0:
                output = []
                break
        if output != []:
            return output

    sys.stderr.write("Moving coverage files...")

    for folder in determin_gcno_folders(current_folder):
        cmd = "find " + folder + "  -name '*.gcno' "
        files = os.popen(cmd).read().split('\n')
        for file in files:
            if os.path.exists(file):
                filename = os.path.splitext(os.path.basename(file))[0]
                data_file = open(file, "rb")
                data = data_file.read()
                template = current_folder + "[\w\d/]*?/" + filename + ".cpp"
                resut = re.search(template, data)
                if resut:
                    location = os.path.dirname(resut.group())
                    dir = os.path.dirname(file)
                    if not (location + '/' + filename + ".cpp" in output):
                        if(os.path.exists(dir + '/' + filename + '.gcda')):
                            cmd = 'cp -f "' + dir + '/' + filename + '.gcno" ' + location + '/ 2>/dev/null'
                            os.system(cmd)
                            cmd = 'cp -f "' + dir + '/' + filename + '.gcda" ' + location + '/ 2>/dev/null'
                            os.system(cmd)
                            output.append(location + '/' + filename + ".cpp")
                data_file.close()
    sys.stderr.write("Moved info about %s files" % len(output))
    return output


def determine_source_folders(current_folder):
    full_path = os.path.abspath(current_folder)
    if full_path.endswith('/osls'):
        folders = filter(lambda x: ((os.path.isdir(x)) and (x != 'output') and (x[0] in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')), os.listdir(full_path))
        folders = map(lambda x: current_folder + '/' + x, folders)
        return folders
    return [current_folder]


def determin_project_folder(current_folder):
    print "Searching for top project folder..."
    project_folder = current_folder
    while (os.path.exists(project_folder + "/../.svn")):
        project_folder = os.path.abspath(os.path.join(project_folder, os.path.pardir))
    return project_folder
    print "The top project folder is: " + project_folder


def determin_gcno_folders(current_folder):
    full_path = os.path.abspath(current_folder)
    res = re.search('/osls(/|$)\w*', full_path)
    if res:
        path = full_path[:res.start()] + '/osls/output/obj/com/emc/cmp' + res.group(0).lower()
        return [path]
    else:
        return [determin_project_folder(current_folder)]


def collect_data(ips, username, password, private_key, path):
    print "Collecting coverage files..."
    import paramiko
    output = []
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    connected = False
    for ip in ips:
        try:
            print "Connecting to " + ip
            ssh.connect(ip, username=username, password=password, key_filename=private_key)
            connected = True

            stdin, stdout, stderr = ssh.exec_command("[ -d " + path + " ] && echo OK")
            if not (stdout.read()):
                continue

            print "Copying coverage files"
            cmd = "scp -r %s@%s:%s /" % (username, ip, path)
            
            os.system(cmd)

            print "Deleting coverage files from system"
            ssh.exec_command("rm -r " + path)
            ssh.close()

            break

        except paramiko.AuthenticationException as e:
            raise SSHError("Can not authenticate to IP address " + ip + " using provided username and password")
        except paramiko.SSHException as e:
            # this is intentional, so the execution continues and attempts to connect to the other host
            pass 
        except Exception as e:
            raise SSHError(str(e))
        finally:
            ssh.close()

    else:
        if connected:
            raise DataFoundError("Folder " + path + " was not found on any ip")
        else:
            raise SSHError("Can not connect to IP addresses " + str(ips))
    print "Coverage files collected"
    return output



    print "\n"


if __name__ == "__main__":
    main()
